Kubernetes Pod 的生命周期
Pod phase(Pod的相位)
Pod 的 status 在信息保存在 PodStatus 中定义,其中有一个 phase 字段。
Pod 的相位(phase)是 Pod 在其生命周期中的简单宏观概述。该阶段并不是对容器或 Pod 的综合汇总,也不是为了做为综合状态机。
在 Kubernetes 中,各个组件都是分布式部署的,围绕着 kube-apiserver 进行通信,那么不同组件之间进行信息同步,就可以通过 status 进行。像 Node 的 status 就记录了该节点的一些状态信息,其他的控制器,就可以通过 status 知道该 Node 的情况,做一些操作,比如节点宕机修复、可分配资源等。
在整个生命周期中,Pod 会出现 5 种状态(相位),分别如下:
- 挂起(Pending):api server 已经创建了 Pod 资源对象,但它尚未被调度完成或者仍处于下载镜像的过程中
- 运行中(Running):Pod 已经被调度至某节点,并且所有容器都已经被 kubelet 创建完成
- 成功(Succeeded):Pod 中的所有容器都已经成功终止并且不会被重启
- 失败(Failed):所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态
- 未知(Unknown):api server 无法正常获取到 Pod 对象的状态信息,通常由网络通信失败所导致
下图是 Pod 的生命周期示意图,从图中可以看到 Pod 状态的变化。
通过 kubectl 创建 Pod 成功后(下面那节),可以通过如下命令看到 Pod 的状态:
# 这里使用了 kubectl 命令行 JSONPATH 模板能力
$ kubectl get pod twocontainers -o=jsonpath='{.status.phase}'
Running
Pod 的生命周期 ⭐
我们一般将 Pod 对象从创建至终的这段时间范围称为 Pod 的生命周期,它主要包含下面的过程:
- pod 创建过程
- 运行初始化容器(init container)过程
- 运行主容器(main container)
- 容器启动后钩子(post start)、容器终止前钩子(pre stop)
- 容器的存活性探测(liveness probe)、就绪性探测(readiness probe)
- pod 终止过程
生命周期如图所示:
pod 的创建过程
- 用户通过 kubectl 或其他 api 客户端提交需要创建的 pod 信息给 apiServer
- apiServer 开始生成 pod 对象的信息,并将信息存入 etcd,然后返回确认信息至客户端
- apiServer 开始反映 etcd 中的 pod 对象的变化,其它组件使用 watch 机制来跟踪检查 apiServer 上的变动
- scheduler 发现有新的 pod 对象要创建,开始为 Pod 分配主机并将结果信息更新至 apiServer
- node 节点上的 kubelet 发现有 pod 调度过来,尝试调用 docker 启动容器,并将结果回送至 apiServer
- apiServer 将接收到的 pod 状态信息存入 etcd 中
pod 的终止过程
- 用户向 apiServer 发送删除 pod 对象的命令
- apiServer 中的 pod 对象信息会随着时间的推移而更新,在宽限期内(默认30s),pod 被视为 dead
- 将 pod 标记为 terminating 状态
- kubelet 在监控到 pod 对象转为 terminating 状态的同时启动 pod 关闭过程
- 端点控制器监控到 pod 对象的关闭行为时将其从所有匹配到此端点的 service 资源的端点列表中移除
- 如果当前 pod 对象定义了 preStop 钩子处理器,则在其标记为 terminating 后即会以同步的方式启动执行
- pod 对象中的容器进程收到停止信号
- 宽限期结束后,若 pod 中还存在仍在运行的进程,那么 pod 对象会收到立即终止的信号
- kubelet 请求 apiServer 将此 pod 资源的宽限期设置为 0 从而完成删除操作,此时 pod 对于用户已不可见
初始化容器阶段
初始化容器是在 pod 的主容器启动之前要运行的容器,主要是做一些主容器的前置工作,它具有两大特征:
- 初始化容器必须运行完成直至结束,若某初始化容器运行失败,那么 kubernetes 需要重启它直到成功完成
- 初始化容器必须按照定义的顺序执行,当且仅当前一个成功之后,后面的一个才能运行
初始化容器有很多的应用场景,下面列出的是最常见的几个:
- 提供主容器镜像中不具备的工具程序或自定义代码
- 初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得到满足
接下来做一个案例,模拟下面这个需求:
- 假设要以主容器来运行 Nginx,但是要求在运行 Nginx 之前要能够连接上 MySQL 和 Redis 所在的服务器。
- 为了简化测试,事先规定好 MySQL 和 Redis 所在的 IP 地址分别为 192.168.18.103 和 192.168.18.104(注意,这两个 IP 都不能 ping 通,因为环境中没有这两个 IP)。
创建 pod-initcontainer.yaml,内容如下:
apiVersion: v1
kind: Pod
metadata:
name: pod-initcontainer
namespace: dev
labels:
user: alsritter
spec:
containers: # 容器配置
- name: nginx
image: nginx:1.17.1
imagePullPolicy: IfNotPresent
ports:
- name: nginx-port
containerPort: 80
protocol: TCP
resources:
limits:
cpu: "2"
memory: "10Gi"
requests:
cpu: "1"
memory: "10Mi"
initContainers: # 初始化容器配置
- name: test-mysql
image: busybox:1.30
command: ["sh","-c","until ping 192.168.18.103 -c 1;do echo waiting for mysql ...;sleep 2;done;"]
securityContext:
privileged: true # 使用特权模式运行容器
- name: test-redis
image: busybox:1.30
command: ["sh","-c","until ping 192.168.18.104 -c 1;do echo waiting for redis ...;sleep 2;done;"]
# 创建pod
$ kubectl create -f pod-initcontainer.yaml
pod/pod-initcontainer created
# 查看pod状态
# 发现pod卡在启动第一个初始化容器过程中,后面的容器不会运行
$ kubectl describe pod pod-initcontainer -n dev
........
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 49s default-scheduler Successfully assigned dev/pod-initcontainer to node1
Normal Pulled 48s kubelet, node1 Container image "busybox:1.30" already present on machine
Normal Created 48s kubelet, node1 Created container test-mysql
Normal Started 48s kubelet, node1 Started container test-mysql
# 动态查看pod
$ kubectl get pods pod-initcontainer -n dev -w
NAME READY STATUS RESTARTS AGE
pod-initcontainer 0/1 Init:0/2 0 15s
pod-initcontainer 0/1 Init:1/2 0 52s
pod-initcontainer 0/1 Init:1/2 0 53s
pod-initcontainer 0/1 PodInitializing 0 89s
pod-initcontainer 1/1 Running 0 90s
接下来,新开一个 shell,为当前服务器(192.168.18.100)新增两个IP,观察Pod的变化:
ifconfig ens33:1 192.168.18.103 netmask 255.255.255.0 up
ifconfig ens33:2 192.168.18.104 netmask 255.255.255.0 up
Pod 的重启策略
Kubernetes 中定义了如下三种重启策略,可以通过 spec.restartPolicy 字段在 Pod 定义中进行设置。
- Always 表示一直重启,这也是默认的重启策略。Kubelet 会定期查询容器的状态,一旦某个容器处于退出状态,就对其执行重启操作;
- OnFailure 表示只有在容器异常退出,即退出码不为 0 时,才会对其进行重启操作;
- Never 表示从不重启;
在 Pod 中设置的重启策略适用于 Pod 内的所有容器。
虽然我们可以设置一些重启策略,确保容器异常退出时可以重启。但是对于运行中的容器,是不是就意味着容器内的服务正常了呢?
比如某些 Java 进程启动速度非常慢,在容器启动阶段其实是无法提供服务的,虽然这个时候该容器是处于运行状态。
再比如,有些服务的进程发生阻塞,导致无法对外提供服务,这个时候容器对外还是显示为运行态。
那么我们该如何解决此类问题呢?有没有一些方法,比如可以通过一些周期性的检查,来确保容器中运行的业务没有任何问题。
所以需要使用到 Pod 的健康检查
Pod 中的健康检查
Kubernetes 中提供了一系列的健康检查,可以定制调用,来帮助解决类似的问题,我们称之为 Probe(探针)。
目前有如下三种 Probe:
1、livenessProbe 可以用来探测容器是否真的在 “运行”,即 “探活”。如果检测失败的话,这个时候 kubelet 就会停掉该容器,容器的后续操作会受到其重启策略的影响。
2、readinessProbe 常常用于指示容器是否可以对外提供正常的服务请求,即 “就绪”,比如 nginx 容器在 reload 配置的时候无法对外提供 HTTP 服务。
3、startupProbe 则可以用于判断容器是否已经启动好,就比如上面提到的容器启动慢的例子。我们可以通过参数,保证有足够长的时间来应对“超长”的启动时间。 如果检测失败的话,同 livenessProbe 的操作。
如果某个 Probe 没有设置的话,我们默认其是成功的。
为了简化一些通用的处理逻辑,Kubernetes 也为这些 Probe 内置了如下三个 Handler:
- ExecAction 可以在容器内执行 shell 脚本;
- HTTPGetAction 方便对指定的端口和 IP 地址执行 HTTP Get 请求;
- TCPSocketAction 可以对指定端口进行 TCP 检查;
在这里 Probe 还提供了其他配置字段,比如 failureThreshold (失败阈值)等,具体参考 配置存活、就绪和启动探针
示例 HTTP GET 探针
将创建一个包含 HTTP GET 存活探针的新 pod
该 pod 的描述文件定义了一个 httpGet 存活探针,该探针告诉 Kubernetes 定期在端口 8080 路径上执行 HTTP GET 请求,以确定该容器是否健康。这些请求在容器运行后立即开始。
可以通过查看 kubectl describe 的内容来了解为什么必须重启容器
可以看到容器现在正在运行,但之前由于错误而终止。
这个退出代码为 137,其有特殊的含义,表示该进程由外部信号终止。数字 137 是两个数字的总和:128+x,其中 x 是终止进程的信号编号。在这个例子中,x 等于 9,即 SIGKILL 的信号编号,意味着这个进程被强行终止。
容器生命周期内的 hook
目前在 Kubernetes 中,有如下两种 hook。
PostStart 可以在容器启动之后就执行。但需要注意的是,此 hook 和容器里的 ENTRYPOINT 命令的执行顺序是不确定的。
PreStop 则在容器被终止之前被执行,是一种阻塞式的方式。执行完成后,Kubelet 才真正开始销毁容器。
同上面的 Probe 一样,hook 也有类似的 Handler:
- Exec 用来执行 Shell 命令;
- HTTPGet 可以执行 HTTP 请求。
我们来看个例子:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
namespace: demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx:1.19
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
可以看出来,我们可以借助 preStop 以优雅的方式停掉 Nginx 服务,从而避免强制停止容器,造成正在处理的请求无法响应。
配置初始化容器 ⭐
在 Kubernetes 中还有一种特殊的容器,即 init 容器。看名字就知道,这个容器工作在正常容器(为了方便区分,我们这里称为应用容器)启动之前,通常用来做一些初始化工作,比如环境检测、OSS 文件下载、工具安装,等等。
应用容器专注于业务处理,其他一些无关的初始化任务就可以放到 init 容器中。这种解耦有利于各自升级,也降低相互依赖。
一个 Pod 中允许有一个或多个 init 容器。init 容器和其他一般的容器非常像,其与众不同的特点主要有:
- 总是运行到完成,可以理解为一次性的任务,不可以运行常驻型任务,因为会 block 应用容器的启动运行;
- 顺序启动执行,下一个的 init 容器都必须在上一个运行成功后才可以启动;
- 禁止使用 readiness/liveness 探针,可以使用 Pod 定义的activeDeadlineSeconds,这其中包含了 Init Container 的启动时间;
- 禁止使用 lifecycle hook。
来看一个 Init 容器的例子
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
namespace: demo
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox:1.31
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox:1.31
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox:1.31
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']
在 myapp-container 启动之前,它会依次启动 init-myservice、init-mydb,分别来检查依赖的服务是否可用。